import abc
import time
import weakref
from threading import RLock
from typing import Any, Callable, Generic, TypeVar

from cion.core.gui.live_timer import LiveTimer

InputType = TypeVar('InputType')
OutputType = TypeVar('OutputType')


class LiveConsumer(Generic[InputType], abc.ABC):
    @abc.abstractmethod
    def send(self, data: InputType):
        pass


class LambdaLiveConsumer(LiveConsumer[InputType]):
    def __init__(self, func: Callable[[InputType], None]):
        self._func = func

    def send(self, data: InputType):
        self._func(data)


class LiveProducer(Generic[OutputType], abc.ABC):
    def __init__(self):
        self._subscribers: set[LiveConsumer[OutputType]] = set()

    # subscribers

    @property
    def subscribers(self) -> frozenset[LiveConsumer[OutputType]]:
        return frozenset(self._subscribers)

    def subscribe(self, subscriber: LiveConsumer[OutputType]) -> LiveConsumer[OutputType]:
        self._subscribers.add(subscriber)
        return subscriber

    def unsubscribe(self, subscriber: LiveConsumer[OutputType]):
        self._subscribers.remove(subscriber)

    # data

    def _produce(self, data: OutputType):
        for subscriber in self._subscribers:
            subscriber.send(data)


class LiveChannel(Generic[InputType, OutputType], LiveConsumer[InputType], LiveProducer[OutputType], abc.ABC):
    """ A channel that can be subscribed """

    def send(self, data: InputType):
        self._produce(self._transform_data(data))

    @abc.abstractmethod
    def _transform_data(self, data: InputType) -> OutputType:
        return data


class IntervalLiveProducer(LiveProducer[OutputType]):
    def __init__(self, prod_func: Callable, *prod_args: Any, interval: float = 1 / 30):
        super().__init__()
        self._prod_func = prod_func
        self._timer = LiveTimer(self._tick_func, weakref.ref(self), *prod_args, interval=interval)

    @staticmethod
    def _tick_func(self_ref, *prod_args):
        self = self_ref()
        if self is None:
            return
        self._produce(self._prod_func(*prod_args))

    @property
    def interval(self):
        return self._timer.interval

    @property
    def alive(self) -> bool:
        return self._timer.alive

    def start(self):
        self._timer.start()

    def stop(self):
        self._timer.stop()

    def join(self):
        self._timer.join()


class IntervalLiveChannel(Generic[InputType, OutputType], LiveConsumer[InputType], LiveProducer[OutputType], abc.ABC):
    """ A BufferedLiveChannel that emits output with certain interval """

    def __init__(self, *, interval: float = 1 / 30):
        super().__init__()
        self._timer = LiveTimer(self._tick_func, weakref.ref(self), interval=interval)
        self._lock = RLock()
        self._dirty = False
        self._wait_time = None
        self._wait_patience = 1  # seconds

    def send(self, data: InputType):
        with self.lock:
            self._update_buffer(data)
            self.notify_update()

    # timer

    @property
    def interval(self) -> float:
        return self._timer.interval

    @property
    def lock(self) -> RLock:
        return self._lock

    @property
    def dirty(self) -> bool:
        return self._dirty

    def notify_update(self):
        with self.lock:
            if self._dirty:
                return
            self._dirty = True
            self._timer.start()

    @staticmethod
    def _tick_func(self_ref):
        self = self_ref()
        if self is None:
            return
        with self._lock:
            if self._dirty:
                data = self._read_buffer()
                self._dirty = False
                self._produce(data)
                self._wait_time = None
            else:
                # Wait for a while instead stopping the timer immediately.
                # This can avoid creating new threads frequently and improve performance.
                if self._wait_time is None:
                    # start waiting
                    self._wait_time = time.time()
                elif time.time() - self._wait_time > self._wait_patience:
                    # stop waiting
                    self._wait_time = None
                    self._timer.stop()
                else:
                    # waiting
                    pass

    # data

    @abc.abstractmethod
    def _update_buffer(self, update: InputType):
        pass

    @abc.abstractmethod
    def _read_buffer(self) -> OutputType:
        pass
